/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.beanSpec;

import java.util.*;
import org.jdesktop.beansbinding.*;
import org.jdesktop.observablecollections.*;

/**
 * Property object for property status binding.<br>
 * This is a read only property!
 * 
 * @param <T> The type of the bean
 * @author M. Hautle
 */
public final class StatusProperty<T> extends PropertyHelper<T, Status> {
	/** The name of the property */
	private final String property;

	/** The base property or null */
	private final Property<T, ? extends StatusInformationInternal> baseProperty;

	/** The map containing all registred map listeners */
	private final IdentityHashMap<T, Listener> map = new IdentityHashMap<T, Listener>();

	/**
	 * Creates a Property object listeneing on a property status.
	 * 
	 * @param <T> The type of the bean
	 * @param path The path to the property (see {@link BeanProperty})
	 * @return A {@link StatusProperty} object for the given path
	 */
	public static <T> StatusProperty<T> create(String path) {
		Property<T, ? extends StatusInformationInternal> p = null;
		final int last = path.lastIndexOf('.');
		if (last > 0) {
			p = BeanProperty.create(path.substring(0, last));
			path = path.substring(last + 1);
		}
		return new StatusProperty<T>(p, path);
	}

	/**
	 * Default constructor
	 * 
	 * @param property The property
	 * @param baseProperty A property object returning the bean in which status/propertystatus your interested in
	 */
	private StatusProperty(final Property<T, ? extends StatusInformationInternal> baseProperty, final String property) {
		this.property = property;
		this.baseProperty = baseProperty;
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#getValue(java.lang.Object)
	 */
	@Override
	public Status getValue(final T source) {
		final Object src = getBase(source);
		return src instanceof StatusInformationInternal ? ((StatusInformationInternal) src)._getBeanStatus().getStatus(property) : null;
	}

	/**
	 * Returns the base object (specified by {@link #baseProperty}) of the given source or source if
	 * {@link #baseProperty} is null.
	 * 
	 * @param source The source object
	 * @return The base object for the status property
	 */
	private Object getBase(final T source) {
		return baseProperty != null ? baseProperty.getValue(source) : source;
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#getWriteType(java.lang.Object)
	 */
	@Override
	public Class<? extends Status> getWriteType(final T source) {
		return null;
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#isReadable(java.lang.Object)
	 */
	@Override
	public boolean isReadable(final T source) {
		return (baseProperty == null || baseProperty.isReadable(source)) && getBase(source) instanceof StatusInformationInternal;
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#isWriteable(java.lang.Object)
	 */
	@Override
	public boolean isWriteable(final T source) {
		return false;
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#setValue(java.lang.Object, java.lang.Object)
	 */
	@Override
	public void setValue(final T source, final Status value) {
		throw new UnsupportedOperationException();
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#listeningStarted(java.lang.Object)
	 */
	@Override
	protected final void listeningStarted(final T source) {
		if (!map.containsKey(source))
			map.put(source, new Listener(source));
	}

	/**
	 * @see org.jdesktop.beansbinding.PropertyHelper#listeningStopped(java.lang.Object)
	 */
	@Override
	protected final void listeningStopped(final T source) {
		final Listener entry = map.remove(source);
		if (entry != null)
			entry.cleanUp();
	}

	/**
	 * Listener for the {@link ObservableMap} holding the status informations about the Bean.<br>
	 * 
	 * @author M. Hautle
	 */
	private class Listener implements ObservableMapListener, PropertyStateListener {
		/** The source bean */
		private final T source;

		/** The map holding the status informations of the bean ({@link StatusInformationInternal#_getBeanStatusMap()}) */
		private ObservableMap sourceMap;

		/** The cached value */
		private Object cachedValue;

		/**
		 * Default constructor
		 * 
		 * @param source The source bean
		 */
		Listener(final T source) {
			this.source = source;
			if (baseProperty != null)
				baseProperty.addPropertyStateListener(source, this);
			setUpListeners();
			cachedValue = sourceMap.get(property);
		}

		/**
		 * Attaches the listeners
		 */
		private void setUpListeners() {
			final Object base = getBase(source);
			if (!(base instanceof StatusInformationInternal))
				throw new IllegalArgumentException("Only classes containing statusinformations are accepted!");
			sourceMap = ((StatusInformationInternal) base)._getBeanStatusMap();
			sourceMap.addObservableMapListener(this);
		}

		/**
		 * Removes the map listener
		 */
		private void removeListeners() {
			sourceMap.removeObservableMapListener(this);
			sourceMap = null;
		}

		/**
		 * Cleans up this listener
		 */
		void cleanUp() {
			removeListeners();
			if (baseProperty != null)
				baseProperty.removePropertyStateListener(source, this);
			cachedValue = null;
		}

		/**
		 * @see org.jdesktop.observablecollections.ObservableMapListener#mapKeyAdded(org.jdesktop.observablecollections.ObservableMap,
		 *      java.lang.Object)
		 */
		public void mapKeyAdded(final ObservableMap map, final Object key) {
			if (property.equals(property))
				updateProperty();
		}

		/**
		 * @see org.jdesktop.observablecollections.ObservableMapListener#mapKeyRemoved(org.jdesktop.observablecollections.ObservableMap,
		 *      java.lang.Object, java.lang.Object)
		 */
		public void mapKeyRemoved(final ObservableMap map, final Object key, final Object value) {
			if (property.equals(key))
				updateProperty();
		}

		/**
		 * @see org.jdesktop.observablecollections.ObservableMapListener#mapKeyValueChanged(org.jdesktop.observablecollections.ObservableMap,
		 *      java.lang.Object, java.lang.Object)
		 */
		public void mapKeyValueChanged(final ObservableMap map, final Object key, final Object lastValue) {
			if (property.equals(key))
				updateProperty();
		}

		/**
		 * Updates the property (if it has changed) and informs the listeners about the change.
		 */
		private void updateProperty() {
			final Object newValue = sourceMap.get(property);
			final Object oldValue = cachedValue;
			if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
				cachedValue = newValue;
				firePropertyStateChange(new PropertyStateEvent(StatusProperty.this, source, true, oldValue, newValue, false, false));
			}
		}

		/**
		 * @see org.jdesktop.beansbinding.PropertyStateListener#propertyStateChanged(org.jdesktop.beansbinding.PropertyStateEvent)
		 */
		public void propertyStateChanged(final PropertyStateEvent pse) {
			if (!pse.getValueChanged())
				return;
			// adapt the listeners
			removeListeners();
			setUpListeners();
			// and update the property
			updateProperty();
		}
	}
}
